﻿using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Threading;
using network;
using shell;

namespace FailoverClusterController
{
    /// <summary>
    /// Essa classe faz as operacoes com as vms.
    /// </summary>
    class Leader
    {
        public Shell shell;
        private ElectionInfo electionInfo;
        private UDPserver serverUDP;

        private System.Diagnostics.EventLog logger;

        public ManualResetEvent noMoreLeader = new ManualResetEvent(false);

        List<Host> hosts;
        Object workLock = new Object();

        string lastMoveSource, lastMoveDest, lastMoveTarget;

        private void log(string str)
        {
            logger.Source = "Leader";
            logger.WriteEntry(str);
        }

        public Leader(System.Diagnostics.EventLog eventLogger, ElectionInfo info)
        {
            logger = eventLogger;

            try
            {
                this.shell = new Shell(
                    "Import-Module FailoverClusters",
                    "Import-Module HyperV",
                    "$HyperVNamespace = \"root\\virtualization\""
                    );
            }
            catch (Exception e)
            {
                log("Error creating Shell: " + e.GetHashCode().ToString() + "\n" + e.Message);
            }

            this.electionInfo = info;
            this.electionInfo.setLeaderHandler +=
                new ElectionInfo.SetLeaderHandler(electionInfo_setLeaderHandler);

            this.serverUDP = new UDPserver();  // leader only
            this.serverUDP.callHandler += new UDPserver.CallHandler(serverUDP_callHandler);

            try
            {
                // coleta os dados das vms de cada node do cluster
                this.getHostList();
            }
            catch (Exception e)
            {
                log("Error getting cluster information: " + e.Message);
            }
            // the first thing to do is verify the VM environment, because
            // may there is some missed task.
            scheduleVMs();
        }

        void electionInfo_setLeaderHandler(ElectionInfo electionInfo, EventArgs args) //called when a new leader is born, and he is not me =]
        {
            log("The King is dead. Long live the King!");
            this.electionInfo.setLeaderHandler -= electionInfo_setLeaderHandler;

            this.serverUDP.callHandler -= serverUDP_callHandler; //unassignment
            this.serverUDP.stop();
            this.noMoreLeader.Set();
        }

        public void serverUDP_callHandler(UDPserver server, MessageArgs args) //messages handler
        {
            log("Request receveid.");

            if (args.messageType == MsgType.FLAVOR_CHANGED)
            {
                // client notifying changes in its machines

                // if it's a bad event, we can ignore
                if (this.isBadEvent(args.messageArgsExtra))
                {
                    log("Bad Event - Ignored.");
                    return;
                }

                lock (this.workLock)
                {
                    // update host information
                    string hostChanged = args.messageArgsExtra[0];
                    this.updateHostList(hostChanged);
                    while (scheduleVMs())
                    {
                        //do nothing
                    }
                }
            }
            else
            {
                // virtual cluster's head node asking for
                // turn on or off some machines based in their
                // base board serial numbers
                var vmguids = args.messageArgsExtra;
                string action = null;
                if (args.messageType == MsgType.START_VM)
                {
                    action = "Start";
                    this.moveBeforeStart(vmguids);
                }
                else if (args.messageType == MsgType.SAVE_VM)
                {
                    action = "Save";
                }

                lock (workLock)
                {
                    signalVM(vmguids, action);
                }
            }
        }

        private void moveBeforeStart(List<string> vmguids)
        {
            Host host_max, host_min;

            foreach (var guid in vmguids)
            {
                this.getMinMaxHosts(out host_max, out host_min);

                // we don't know where is the vm, so we need to search in all the hosts
                string q = "Get-VMHost | " +
                "%{{Get-WmiObject -namespace root\\Virtualization " +
                "-class Msvm_VirtualSystemSettingData " +
                "-filter \"BIOSGUID='{{{0}}}'\" -computername $_}}";

                PSObject res = shell.run(q, guid)[0];
                String vmName = res.Properties["ElementName"].Value.ToString();
                String hostSource = res.Properties["__SERVER"].Value.ToString();

                // here we move the vm to host_min,
                // and, at the same time, get its memory value to update the host information
                PSObject m = shell.run(
                    "Get-VMClusterGroup -vm {0} -server {1} | Move-ClusterGroup -node {2}" + // move
                    " | %{{Get-VMMemory $_.Name -server $_.OwnerNode}}", // use the result to get memory info
                    vmName, hostSource, host_min.name)[0];

                // get the memory
                UInt64 mem = Convert.ToUInt64(m.Properties["VirtualQuantity"].Value.ToString());
                host_min.AddVM(new VirtualMachine(vmName, mem));
            }

        }

        // test if this is a event created by a
        // a migration made by the Leader.
        private bool isBadEvent(List<string> eventInfo)
        {
            var hostname = eventInfo[0];
            var vmname = eventInfo[1];
            var vmstate = eventInfo[2];

            if (vmname != this.lastMoveTarget) return false;
            if ((hostname.ToLower() == lastMoveSource.ToLower() && vmstate == "3") ||
                (hostname.ToLower() == lastMoveDest.ToLower() && vmstate == "2"))
            {
                return true;
            }
            return false;

        }

        /// <summary>
        /// Analisa o cenario de VMs e verifica
        /// se eh preciso fazer alguma migracao
        /// para balancear a carga total do sistema.
        /// </summary>
        public bool scheduleVMs()
        {
            // agora, escolhemos os hosts com maior e menor carga
            Host host_max, host_min;

            this.getMinMaxHosts(out host_max, out host_min);

            if (host_max.vms.Count - host_min.vms.Count <= 1)
            {
                return false;
            }

            // agora, encontramos a VM mais leve no
            // host mais carregado
            VirtualMachine vm = host_max.getLightest();

            // e movemos ela de um host pra outro
            if (vm == null)
            {
                return false;
            }

            // move the vm.
            // TODO: catch migration error.
            this.moveVM(vm.name, host_max.name, host_min.name);

            // update host status list
            host_max.RemoveVM(vm);
            host_min.AddVM(vm);

            // a ideia eh retornar false caso naum tenha migrado
            // nenhuma maquina de lugar, o que pode acontecer
            // se tal operacao nao valer a pena.
            // TODO: definir quando fazer ou nao a migracao.
            // TODO: nos casos de ligar VMs, executar isso
            // antes para mover com mais facilidade.
            return true;

        }

        private void getMinMaxHosts(out Host host_max, out Host host_min)
        {
            int max = 0, min = 0;
            for (int i = 1; i < this.hosts.Count; i++)
            {
                if (this.hosts[i].usedMemRelative > this.hosts[max].usedMemRelative)
                    max = i;

                if (this.hosts[i].usedMemRelative < this.hosts[min].usedMemRelative)
                    min = i;
            }
            host_max = this.hosts[max];
            host_min = this.hosts[min];
        }


        /// <summary>
        /// Executa uma acao num grupo de VMs fornecido:
        /// Start, Stop e Save
        /// </summary>
        /// <param name="VMBIOSGuid">
        /// Um vetor com os BIOS Guids das VMs
        /// que dever ser ligadas/desligadas.</param>
        /// <param name="action">Uma opcao entre "Start", "Stop" ou "Save"</param>
        public void signalVM(List<String> VMBIOSGuid, String action)
        {
            foreach (var Guid in VMBIOSGuid)
            {
                // we don't know where the vm is, so we need to search in all hosts
                string q = "Get-VMHost |  " +
                "%{{Get-WmiObject -namespace root\\Virtualization " +
                "-class Msvm_VirtualSystemSettingData " +
                "-filter \"BIOSGUID='{{{0}}}'\" -computername $_}}";

                PSObject res = shell.run(q, Guid)[0];
                String vmName = res.Properties["ElementName"].Value.ToString();
                String vmHost = res.Properties["__SERVER"].Value.ToString();

                shell.run("{2}-VM -vm {0} -server {1} -force -wait",
                    vmName, vmHost, action);
            }

        }

        /// <summary>
        /// Move uma VM de um host para outro.
        /// </summary>
        /// <param name="vmName">Nome da VM</param>
        /// <param name="origin">Nome do host de origem</param>
        /// <param name="destination">Nome do host de destino</param>
        public void moveVM(string vmName, string origin, string destination)
        {
            // we'll cache last move information
            this.lastMoveDest = destination;
            this.lastMoveSource = origin;
            this.lastMoveTarget = vmName;

            try
            {
                shell.run("get-vm -vm {0} -server {1} | Move-VM -destination {2} -force",
                vmName, origin, destination);
            }
            catch (ShellException e)
            {
                log(String.Format("Migration Error: {0}", e.Message));
                //<<TODO>> refazer a migração, dessa vez pra outra máquina
            }
            catch (RuntimeException ex)
            {
                log(String.Format("RunTime Error: {0}", ex.Message));
            }
        }

        /// <summary>
        /// Coletamos os dados sobre as maquinas virtuais em cada
        /// node do cluster, e calculamos a memoria utilizada por
        /// cada uma. Calculamos apenas com base nas VMs ligadas.
        /// </summary>
        public void getHostList()
        {
            this.hosts = new List<Host>();
            foreach (PSObject result in shell.run("Get-ClusterNode | Where {{$_.state -eq 'Up'}} | %{{$_.Name}}"))
            {
                var server = getMemoryState(result.ToString());
                this.hosts.Add(server);
            }
        }

        private void updateHostList(string hostChanged)
        {
            var i = this.hosts.FindIndex(host => host.name.ToLower() == hostChanged.ToLower());
            this.hosts[i] = this.getMemoryState(this.hosts[i].name);
        }

        // TODO: move this method to Host class.
        public Host getMemoryState(String hostName)
        {
            Host server = new Host(hostName);
            foreach (PSObject res in shell.run("Get-VM -server {0} -running", server.name))
            {
                String vmName = res.Properties["VMElementName"].Value.ToString();
                PSObject m = shell.run("Get-VMMemory -Server {0} -VM {1}", server.name, vmName)[0];
                UInt64 mem = Convert.ToUInt64(m.Properties["VirtualQuantity"].Value.ToString());
                VirtualMachine machine = new VirtualMachine(vmName, mem);

                server.AddVM(machine);
            }
            return server;
        }
    } // class Leader
}

